﻿using System;
using System.Collections.Generic;
using System.Text;

namespace Glyphica
{
    class MapBuilder
    {
        // The map array
        private MapTile[,] Tiles;
                        
        #region Public Members

        public int MapHeight;
        public int MapWidth;
        public int MinRooms;
        public int MaxRooms;
        public int MinCorridorLength;
        public int MaxCorridorLength;
        public int MinRoomSize;
        public int MaxRoomSize;
        public int RoomSpacing;

        #endregion

        #region Private Variables

        // These lists don't serve a specific purpose as of yet, but future improvements to the
        // map generation code will necessitate them, so I'm leaving them here for now.
        private List<Corridor> Corridors;
        private List<Room> Rooms;

        // class-level randomizer that is used so that dungeons can be re-generated from specific seeds
        Random rand;
        
        #endregion

        public MapBuilder(Random r)
        {
            this.rand = r;
        }

        #region Map generation methods

        public MapLevel GenerateMap()
        {
            // initialize properties
            // If they have not been explicitly set, use default values
            MapHeight = (MapHeight == 0) ? 200 : MapHeight;
            MapWidth = (MapWidth == 0) ? 400 : MapWidth;
            MinRooms = (MinRooms == 0) ? 20 : MinRooms;
            MaxRooms = (MaxRooms == 0) ? 50 : MaxRooms;
            MinCorridorLength = (MinCorridorLength == 0) ? 4 : MinCorridorLength;
            MaxCorridorLength = (MaxCorridorLength == 0) ? 8 : MaxCorridorLength;
            MinRoomSize = (MinRoomSize == 0) ? 4 : MinRoomSize;
            MaxRoomSize = (MaxRoomSize == 0) ? 8 : MaxRoomSize;
            RoomSpacing = (RoomSpacing == 0) ? 2 : RoomSpacing;

            // initialize lists of dungeon features
            Corridors = new List<Corridor>();
            Rooms = new List<Room>();

            // initialize the map array
            Tiles = new MapTile[MapHeight, MapWidth];
            for (int row = 0; row < Tiles.GetLength(0); row++)
            {
                for (int column = 0; column < Tiles.GetLength(1); column++)
                {
                    Tiles[row, column] = new MapTile(MapTile.TileType.Stone);
                }
                Console.WriteLine();
            }
            
            // put a room in the center of the map
            MapPoint tl = new MapPoint(MapHeight / 2, MapWidth / 2);
            MapPoint br = new MapPoint(tl.Row + 4, tl.Column + 4);
            Room StartingRoom = new Room(new Rectangle(tl, br));
            Rooms.Add(StartingRoom);
            PaintRectangle(StartingRoom.Area);

            // Now, add features until we have reached the specified number of rooms            
            BuildPoint cbp;  // corridor build point
            BuildPoint rbp;  // room build point
            int NumRooms = rand.Next(MinRooms, MaxRooms);
            do
            {
                // what type of feature are we going to build?
                Corridor NewCorridor;
                Room NewRoom;
                switch (GetNextFeatureType())
                {
                    case FeatureType.Corridor_DeadEnd:
                        // A corridor that goes nowhere.
                        cbp = PickBuildPoint();
                        NewCorridor = CreateCorridor(cbp);
                        if (NewCorridor != null)
                        {
                            SetTile(cbp, MapTile.TileType.Door_Open);  // all doors are set to open by default
                            PaintRectangle(NewCorridor.Area);
                            Corridors.Add(NewCorridor);
                        }
                        break;

                    case FeatureType.Corridor_NewRoom:
                        // A corridor with a room at the end.
                        cbp = PickBuildPoint();
                        NewCorridor = CreateCorridor(cbp);
                        if (NewCorridor != null)
                        {
                            // We've got a valid corridor.  Now we need to try and hang a room off the end of it.
                            // Create a new BuildPoint at the far end of the corridor.
                            rbp = new BuildPoint(cbp.Row,cbp.Column,cbp.Direction);
                            switch (rbp.Direction)
                            {
                                case BuildDirection.East:
                                    rbp.Column = NewCorridor.Area.BottomRight.Column + 1;
                                    break;
                                case BuildDirection.North:
                                    rbp.Row = NewCorridor.Area.TopLeft.Row - 1;
                                    break;
                                case BuildDirection.South:
                                    rbp.Row = NewCorridor.Area.BottomRight.Row + 1;
                                    break;
                                case BuildDirection.West:
                                    rbp.Column = NewCorridor.Area.TopLeft.Column - 1;
                                    break;
                            }

                            // Try to build a room from the new Build Point
                            NewRoom = CreateRoom(rbp);
                            if (NewRoom != null)
                            {
                                SetTile(rbp, MapTile.TileType.Door_Open);  // all doors are set to open by default
                                PaintRectangle(NewRoom.Area);
                                Rooms.Add(NewRoom);

                                SetTile(cbp, MapTile.TileType.Door_Open);  // all doors are set to open by default
                                PaintRectangle(NewCorridor.Area);
                                Corridors.Add(NewCorridor);
                            }
                        }
                        break;

                    case FeatureType.Room:
                        // A room with no hallway leading to it
                        rbp = PickBuildPoint();
                        NewRoom = CreateRoom(rbp);
                        if (NewRoom != null)
                        {
                            SetTile(rbp, MapTile.TileType.Door_Open);  // all doors are set to open by default
                            PaintRectangle(NewRoom.Area);
                            Rooms.Add(NewRoom);
                        }
                        break;
                }
            }
            while (Rooms.Count < NumRooms);

            PreProcessVisibility(ref Tiles);

            MapLevel ReturnValue = new MapLevel();
            ReturnValue.Tiles = Tiles;
            ReturnValue.StairsDown = FindRandomTile(MapTile.TileType.Empty);
            ReturnValue.StairsUp = FindRandomTile(MapTile.TileType.Empty);
            ReturnValue.Height = MapHeight;
            ReturnValue.Width = MapWidth;
            return ReturnValue;
        }

        private void SetTile(MapPoint location, MapTile.TileType type)
        {
            // Set a specific tile to be a specific type of tile
            Tiles[location.Row, location.Column].Type = type;
        }

        private Room CreateRoom(BuildPoint origin)
        {
            // what are the dimensions of the room?
            int RoomHeight = rand.Next(MinRoomSize, MaxRoomSize);
            int RoomWidth = rand.Next(MinRoomSize, MaxRoomSize);

            // for a room, the area to check is the room itself
            Rectangle AreaToCheck;
            MapPoint tl = null;
            MapPoint br = null;

            switch (origin.Direction)
            {
                case BuildDirection.West:
                    tl = new MapPoint(origin.Row - (RoomHeight / 2)- RoomSpacing, origin.Column - RoomWidth - RoomSpacing);
                    br = new MapPoint(tl.Row + RoomHeight + RoomSpacing, origin.Column - 1);
                    break;

                case BuildDirection.East:
                    tl = new MapPoint(origin.Row - (RoomHeight / 2) - RoomSpacing, origin.Column + 1);
                    br = new MapPoint(tl.Row + RoomHeight + RoomSpacing, origin.Column + RoomWidth + RoomSpacing);
                    break;

                case BuildDirection.North:
                    tl = new MapPoint(origin.Row - RoomHeight - RoomSpacing, origin.Column - (RoomWidth / 2) - RoomSpacing);
                    br = new MapPoint(origin.Row - 1, tl.Column + RoomWidth + RoomSpacing);
                    break;

                case BuildDirection.South:
                    tl = new MapPoint(origin.Row + 1, origin.Column - (RoomWidth / 2) - RoomSpacing);
                    br = new MapPoint(origin.Row + RoomHeight + RoomSpacing, tl.Column + RoomWidth + RoomSpacing);
                    break;
            }

            // Are both corners of the area within the map?
            if (IsValidMapPoint(tl) && IsValidMapPoint(br))
            {
                // Is there another dungeon feature in the area?
                AreaToCheck = new Rectangle(tl, br);
                if (IsAreaClear(AreaToCheck))
                {
                    // at this point, we have a valid room
                    // apply the spacing to shrink the room appropriately
                    Rectangle AreaToBuild = new Rectangle(AreaToCheck.TopLeft, AreaToCheck.BottomRight);
                    switch (origin.Direction)
                    {
                        case BuildDirection.West:
                            AreaToBuild.TopLeft.Row += RoomSpacing;
                            AreaToBuild.TopLeft.Column += RoomSpacing;
                            AreaToBuild.BottomRight.Row -= RoomSpacing;
                            break;
                        case BuildDirection.East:
                            AreaToBuild.TopLeft.Row += RoomSpacing;
                            AreaToBuild.BottomRight.Row -= RoomSpacing;
                            AreaToBuild.BottomRight.Column -= RoomSpacing;
                            break;
                        case BuildDirection.North:
                            AreaToBuild.TopLeft.Row += RoomSpacing;
                            AreaToBuild.TopLeft.Column += RoomSpacing;
                            AreaToBuild.BottomRight.Column -= RoomSpacing;
                            break;
                        case BuildDirection.South:
                            AreaToBuild.TopLeft.Column += RoomSpacing;
                            AreaToBuild.BottomRight.Row -= RoomSpacing;
                            AreaToBuild.BottomRight.Column -= RoomSpacing;
                            break;
                    }

                    Room ThisRoom = new Room(AreaToBuild);
                    return ThisRoom;
                }
                else
                {
                    return null;  // area is not clear
                }
            }
            else
            {
                return null;  // room corners run off the edge of the map
            }
        }

        private Corridor CreateCorridor(BuildPoint origin)
        {
            // how long should the corridor be?
            int CorridorLength = rand.Next(MinCorridorLength, MaxCorridorLength);

            // For a corridor, the area to check is a three-tile wide rectangle
            // to ensure that the corridor doesn't "cave into" an existing dungeon feature
            Rectangle AreaToCheck;
            MapPoint tl = null;
            MapPoint br = null;

            switch (origin.Direction)
            {
                case BuildDirection.West:
                    tl = new MapPoint(origin.Row - 1, origin.Column - CorridorLength);
                    br = new MapPoint(origin.Row + 1, origin.Column - 1);
                    break;

                case BuildDirection.East:
                    tl = new MapPoint(origin.Row - 1, origin.Column + 1);
                    br = new MapPoint(origin.Row + 1, origin.Column + CorridorLength);
                    break;

                case BuildDirection.North:
                    tl = new MapPoint(origin.Row - CorridorLength, origin.Column - 1);
                    br = new MapPoint(origin.Row - 1, origin.Column + 1);
                    break;

                case BuildDirection.South:
                    tl = new MapPoint(origin.Row + 1, origin.Column - 1);
                    br = new MapPoint(origin.Row + CorridorLength, origin.Column + 1);
                    break;
            }

            // Are both corners of the area within the map?
            if (IsValidMapPoint(tl) && IsValidMapPoint(br))
            {
                // Is there another dungeon feature in the area?
                AreaToCheck = new Rectangle(tl, br);
                if (IsAreaClear(AreaToCheck))
                {
                    // At this point, we have a good corridor
                    // squeeze the area to a one-tile hallway
                    if (origin.Direction == BuildDirection.East || origin.Direction == BuildDirection.West)
                    {
                        AreaToCheck.TopLeft.Row++;
                        AreaToCheck.BottomRight.Row--;
                    }
                    else
                    {
                        AreaToCheck.TopLeft.Column++;
                        AreaToCheck.BottomRight.Column--;
                    }
                    Corridor ThisCorridor = new Corridor(AreaToCheck);
                    return ThisCorridor;
                }
                else
                {
                    return null;  //area is not clear
                }
            }
            else
            {
                return null;  // corridor runs off the edge of the map
            }
        }

        private void PaintRectangle(Rectangle area)
        {
            // Carve an empty space in the specified rectangle
            for (int row = area.TopLeft.Row; row <= area.BottomRight.Row; row++)
            {
                for (int column = area.TopLeft.Column; column <= area.BottomRight.Column; column++)
                {
                    Tiles[row, column].Type = MapTile.TileType.Empty;
                }
            }
        }

        private bool IsAreaClear(Rectangle area)
        {
            for (int row = area.TopLeft.Row; row <= area.BottomRight.Row; row++)
            {
                for (int column = area.TopLeft.Column; column <= area.BottomRight.Column; column++)
                {
                    if (this.Tiles[row, column].Type==MapTile.TileType.Empty)
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        private BuildPoint PickBuildPoint()
        {
            // Find a map tile that is:
            // 1. Not open
            // 2. Adjacent to a tile that IS open
            // The non-open tile becomes the point at which a new feature is added to the dungeon
            BuildPoint ReturnValue = null;
            do
            {
                int row = rand.Next(1, MapHeight - 2);
                int column = rand.Next(1, MapWidth - 2);

                if (Tiles[row, column].Type==MapTile.TileType.Empty)
                {
                    if (Tiles[row - 1, column].Type == MapTile.TileType.Stone)
                    {
                        ReturnValue = new BuildPoint(row - 1, column, BuildDirection.North);
                        break;
                    }

                    if (Tiles[row + 1, column].Type == MapTile.TileType.Stone)
                    {
                        ReturnValue = new BuildPoint(row + 1, column, BuildDirection.South);
                        break;
                    }

                    if (Tiles[row, column - 1].Type == MapTile.TileType.Stone)
                    {
                        ReturnValue = new BuildPoint(row, column - 1, BuildDirection.West);
                        break;
                    }

                    if (Tiles[row, column + 1].Type == MapTile.TileType.Stone)
                    {
                        ReturnValue = new BuildPoint(row, column + 1, BuildDirection.East);
                        break;
                    }
                }

            }
            while (ReturnValue == null);

            return ReturnValue;
        }

        private bool IsValidMapPoint(MapPoint location)
        {
            if (location.Row > 0 && location.Row < MapHeight - 1 && location.Column > 0 && location.Column < MapWidth - 1)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        private MapPoint FindRandomTile(MapTile.TileType TileType)
        {
            int row;
            int column;
            do
            {
                row = rand.Next(0,MapHeight-1);
                column = rand.Next(0,MapWidth-1);
            }
            while(this.Tiles[row,column].Type!=TileType);

            return new MapPoint(row, column);
        }


        private FeatureType GetNextFeatureType()
        {
            FeatureType ReturnValue = FeatureType.Corridor_NewRoom;
            int r = rand.Next(1, 100);

            if (r <= 75)
            {
                if (r <= 5) { ReturnValue = FeatureType.Corridor_DeadEnd; }
                if (r >= 6) { ReturnValue = FeatureType.Corridor_NewRoom; }
            }
            if (r >= 76) { ReturnValue = FeatureType.Room; }

            return ReturnValue;
        }

        private void PreProcessVisibility(ref MapTile[,] map)
        {
            // The primary purpose of this function is to hide all of the stone that isn't directly adjacent
            // to an empty space.  This give the map a cleaner look and RogueLike feel.
            for (int row = 0; row < map.GetLength(0); row++)
            {
                for (int column = 0; column < map.GetLength(1); column++)
                {
                    MapTile ThisTile = map[row, column];
                    if (ThisTile.Type == MapTile.TileType.Stone)
                    {
                        ThisTile.IsVisible = false;
                    }

                    if (ThisTile.Type == MapTile.TileType.Stone)
                    {
                        if (row > 0)
                        {
                            if (column > 0)
                            {
                                if (map[row - 1, column - 1].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }
                            }

                            if (map[row - 1, column].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }

                            if (column < map.GetLength(1) - 1)
                            {
                                if (map[row - 1, column + 1].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }
                            }
                        }

                        if (column > 0)
                        {
                            if (map[row, column - 1].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }
                        }

                        if (column < map.GetLength(1) - 1)
                        {
                            if (map[row, column + 1].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }
                        }

                        if (row < map.GetLength(0) - 1)
                        {
                            if (column > 0)
                            {
                                if (map[row + 1, column - 1].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }
                            }

                            if (map[row + 1, column].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }
                            if (column < map.GetLength(1) - 1)
                            {
                                if (map[row + 1, column + 1].Type == MapTile.TileType.Empty) { ThisTile.IsVisible = true; }
                            }
                        }
                    }

                    if (!ThisTile.IsVisible)
                    {
                        SetTile(new MapPoint(row, column), MapTile.TileType.Stone_NeverVisible);
                    }
                }
            }
        }

        #endregion
        
        #region Private items used for map generation

        private class Rectangle
        {
            public MapPoint TopLeft;
            public MapPoint BottomRight;
            public Rectangle(MapPoint topleft, MapPoint bottomright)
            {
                this.TopLeft = topleft;
                this.BottomRight = bottomright;
            }
        }

        private class Room
        {
            public Rectangle Area;
            public Room(Rectangle area)
            {
                this.Area = area;
            }
        }

        private class Corridor
        {
            public Rectangle Area;

            public Corridor(Rectangle area)
            {
                this.Area = area;
            }
        }

        public enum BuildDirection
        {
            North,
            South,
            East,
            West
        }

        private enum FeatureType
        {
            Room,
            Corridor_DeadEnd,
            Corridor_NewRoom
        }

        #endregion

    }
}
